iT邦幫忙

2025 iThome 鐵人賽

DAY 3
0
Rust

30天解鎖 Rust 開發者工具箱系列 第 3

「Day 03」精確收納:cargo module

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250917/20177832ZHW3QCMv79.jpg
圖:“Rust 的吉祥物 Ferris the Crab 把紙箱整理放進去大木箱裡”,gemini-2.5-flash-preview,2025年09月17日。

前言:在框架中嘗試,在框架中成長

都說了 Rust 重編譯,cargo 專案提供了強大穩定的骨架,本篇會實作我是如何將小元件的實作,成長到模組化,希望帶出其結構邊界,讓開發可以在可控的邊界內成長。

另外,值得在這系列文章的開端提一提的就是在現在 AI LLM coding 的時代下,我個人有個小小的技法,就是從抽象海洋中往下逐一實例化。AI 有多強無所謂,開發者還是人,人還是人,之所以讓開發者沈浸其中的開發工作,不就是將腦海中很跳躍的創意想法,一步一腳印把想法給實作建構起來嗎?那現在還是所謂 “Agentic AI” 的時代(?,能夠幫你寫程式碼,還能夠幫你下指令執行,錯誤會自己看自己修正,往復循環直到成功了才提醒你過來看。身為 Rust 開發者,最最最重要的是它幫你擋下編譯器的炮口了,終於...可以擺脫被 Rust 編譯器支配的恐懼,愛死你了~

我會儘量讓我的系列文章從抽象海洋描述,再到示意圖與結構,最後才是實例化的程式碼範例。當工程師遇到文件,心裡慌得一批,我需要模板...沒事!自己先建構這個樣子試試!

開發情景:從模組到箱子(crate)

等等要實作的專案結構先破題:

cargo_tutorial/
├── Cargo.toml                 # 專案配置
├── src/                       
│   ├── lib.rs                 # 程式庫根檔案
│   ├── main.rs                # 應用程式入口(執行檔入口)
│   ├── csv_converter/         # 模組 module (mod)
│   │   ├── mod.rs             # 模組介面
│   │   └── converter.rs       # 邏輯實作
│   └── test_data_generator.rs # 獨立邏輯實作,亦可以視為模組
└── README.md                  # 記得!寫文件!XD

上圖解釋:
https://ithelp.ithome.com.tw/upload/images/20250917/201778329GusUST405.png
想像你的整個專案(cargo new cargo_tutorial)就是一個大木箱 crate,這是 Rust 編譯的最小單位,lib.rs 就是貼在木箱的便利貼,告訴你箱子裡面有什麼模組可以拿來用,main.rs 告訴你編譯後我就是執行檔入口,沒有 main.rs 就是函式庫的專案,在 Rust 中的各種依賴就是如此。

大箱子內的小紙箱就是模組 module mod 關鍵字,一個檔案、一個資料夾都可以成為一個模組,模組內也可以有子模組,以此類推。

骨架(一):模組化

專案結構

cargo_tutorial/
├── Cargo.toml                 # 專案配置
├── src/                       
│   ├── lib.rs                 # 程式庫根檔案
│   ├── main.rs                # 應用程式入口(執行檔入口)
│   ├── csv_converter/         # 模組 module (mod)
│   │   ├── mod.rs             # 模組介面
│   │   └── converter.rs       # 邏輯實作
│   └── test_data_generator.rs # 獨立邏輯實作,亦可以視為模組
└── README.md                  # 記得!寫文件!XD

程式執行流程圖

流程開始
    ↓
main() 應用程式入口:軟體啟動!
    ↓
呼叫 create_sample_csv_file("demo.csv", 50)
    ├─► 建立 CSV Writer
    ├─► 寫入標題列 ["id", "name", "age", "city", "value", "active"]
    ├─► 產生 50 筆測試資料迴圈
    │   ├─► 生成使用者名稱、年齡、城市等欄位
    │   └─► 寫入每一行資料
    └─► 關閉檔案並返回成功
    ↓
呼叫 CsvConverter::convert_csv_to_json_file("demo.csv", "demo.json")
    ├─► 開啟 demo.csv 檔案
    ├─► 建立 CSV 讀取器
    ├─► 讀取標題列
    ├─► 處理每一行資料迴圈
    │   ├─► 將每一欄位轉換為適當的 JSON 型別
    │   │   ├─► 整數 → Number
    │   │   ├─► 浮點數 → Number
    │   │   ├─► "true"/"false" → Boolean
    │   │   └─► 其他 → String
    │   └─► 建立鍵值對 HashMap
    ├─► 將所有記錄序列化為 JSON 字串
    ├─► 寫入 demo.json 檔案
    └─► 返回成功
    ↓
流程結束,查看第一個函數所生成的 CSV 檔案是否有順利被第二個函數轉換成 JSON 檔案;demo.csv 與 demo.json

原始碼

# Cargo.toml

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
csv = "1.3"
anyhow = "1.0"
chrono = { version = "0.4", features = ["serde"] }
// src/main.rs

// 模組引入
mod csv_converter;
mod test_data_generator;

// 模組使用
use csv_converter::CsvConverter;
use test_data_generator::create_sample_csv_file;

fn main() -> std::io::Result<()> {
    // 生成測試用 CSV 檔案,讓接下來的函數有 CSV 資料檔案使用
    create_sample_csv_file("demo.csv", 50)?;

    // 將 CSV 轉換為 JSON 格式,並儲存為 demo.json 檔案
    CsvConverter::convert_csv_to_json_file("demo.csv", "demo.json")?;
    Ok(())
}
// src/test_data_generator.rs

use std::fs::File;
use csv::Writer;

pub fn create_sample_csv_file(filename: &str, record_count: usize) -> std::io::Result<()> {
    let file = File::create(filename)?;
    let mut writer = Writer::from_writer(file);

    // 寫入 CSV 標題列
    writer.write_record(&["id", "name", "age", "city", "value", "active"])?;
    let cities = ["New York", "London", "Tokyo", "Paris"];

    // 產生測試記錄(為簡單起見使用確定性生成)
    for i in 1..=record_count {
        let name = format!("User{}", i);
        let age = 20 + (i % 50); 
        let city = cities[i % cities.len()];
        let value = 100.0 + (i as f64 * 10.0);
        let active = (i % 3) != 0;

        writer.write_record(&[
            &i.to_string(),
            &name,
            &age.to_string(),
            city,
            &format!("{:.2}", value),
            &active.to_string()
        ])?;
    }

    writer.flush()?;
    Ok(())
}
// src/csv_converter/converter.rs

use csv::Reader;
use std::fs::File;
use std::collections::HashMap;

// 宣告公開結構體,讓外部的 crate 或其他模組可以存取 CsvConverter 功能
pub struct CsvConverter;

// 實作 CsvConverter 結構體的方法
impl CsvConverter {
    /// 將 CSV 檔案轉換並儲存為 JSON 檔案
    pub fn convert_csv_to_json_file(csv_path: &str, json_path: &str) -> std::io::Result<()> {
        // 開啟 CSV 檔案
        let file = File::open(csv_path)?;
        let mut reader = Reader::from_reader(file);

        // 讀取標題行
        let headers: Vec<String> = reader.headers()?
            .iter()
            .map(|h| h.to_string())
            .collect();

        let mut records = Vec::new();

        // 處理每一行資料
        for result in reader.records() {
            let record = result?;
            let mut row_map = HashMap::new();

            // 將每一行轉換為鍵值對
            for (i, field) in record.iter().enumerate() {
                if i < headers.len() {
                    let header = &headers[i];
                    let value = if let Ok(num) = field.parse::<i64>() {
                        serde_json::Value::Number(num.into())
                    } else if let Ok(num) = field.parse::<f64>() {
                        if let Some(n) = serde_json::Number::from_f64(num) {
                            serde_json::Value::Number(n)
                        } else {
                            serde_json::Value::String(field.to_string())
                        }
                    } else if field.to_lowercase() == "true" {
                        serde_json::Value::Bool(true)
                    } else if field.to_lowercase() == "false" {
                        serde_json::Value::Bool(false)
                    } else {
                        serde_json::Value::String(field.to_string())
                    };

                    row_map.insert(header.clone(), value);
                }
            }

            records.push(row_map);
        }

        // 將記錄序列化為 JSON
        let json_string = serde_json::to_string_pretty(&records)?;

        // 輸出 JSON 檔案
        std::fs::write(json_path, &json_string)?;

        Ok(())
    }
}
// src/csv_converter/mod.rs

// 宣告子模組,告訴編譯器在同一個目錄中尋找 converter.rs 檔案
mod converter;

// 重新匯出所有公開項目,讓外部可以使用這個模組的公開介面
pub use converter::*;
// src/lib.rs

// 宣告公開模組,讓外部 crate 可以存取 csv_converter 功能
pub mod csv_converter;

// 重新匯出核心功能,讓使用者更容易使用
pub use csv_converter::CsvConverter;

lib.rsmod.rs 撰寫規則

語法上這兩者幾乎是一樣的,但各自的角色和作用域是不一樣的,一個大木箱 crate 或嚴謹一點稱之為 crate root 中只能有一個 lib.rs,很好理解,當然只能有一份能夠宣告我有什麼可以拿來用。mod.rs 是在這個大木箱內可能有很多個小紙箱模組 modmodule root 的空間宣告你可以從我這個小紙箱拿什麼東西出來用。

簡單來說,你開發的專案 crate 要給別人用的時候,lib.rs 對外,mod.rs 對內,編譯到你的 crate 的時候,編譯器會先找到 lib.rs,根據上面寫的進行編譯。

// 找到 module_name.rs 或 module_name/mod.rs
pub mod module_name;  
// 找到 internal.rs 或 internal/mod.rs,但不對外公開使用,作用域是在整個 crate 內部,因為現在只有 src/ 所呈現的一個 crate,在非 workspace 情況下呈現不出私有的感覺
mod internal;

// 重新匯出特定型別,結構體實作:pub struct SomeType
pub use module_name::SomeType;
// 重新匯出特定函數,功能實作:pub fn some_function(){}
pub use module_name::some_function; 
// 重新匯出所有公開項目 (萬用字元)
pub use module_name::*;             

上一篇
「Day 02」Cargo Is All You Need
系列文
30天解鎖 Rust 開發者工具箱3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言